package org.beynet.jsonbinding.bind;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.List;

import org.apache.log4j.Logger;
import org.beynet.jsonbinding.parser.JSONParsingException;
import org.beynet.jsonbinding.parser.types.JSONArray;
import org.beynet.jsonbinding.parser.types.JSONMember;
import org.beynet.jsonbinding.parser.types.JSONObject;
import org.beynet.jsonbinding.parser.types.JSONParser;
import org.beynet.jsonbinding.parser.types.impl.JSONParserImpl;

public class JSONBUnmarshaller {
    
    JSONBUnmarshaller(JSONBContext context) {
        this.context = context ;
    }
    
    public Object unmarshall(String encoding,InputStream is) throws JSONParsingException, IOException, JSONBindingException {
        JSONParser parser = new JSONParserImpl();
        Object resultOfParsing = parser.parse(encoding,is);
        if (resultOfParsing instanceof JSONArray) {
            throw new JSONBindingException("Could not bind arrays into object");
        }
        JSONObject jsonObject = (JSONObject)resultOfParsing;
        if (jsonObject.getMembers().size()>1) {
            throw new JSONBindingException("only one objet supported at root level");
        }
        JSONMember rootElement = jsonObject.getMembers().get(0);
        if (!JSONObject.class.isAssignableFrom(rootElement.getValue().getClass())) {
            throw new JSONBindingException("root element must be an object");
        }
        JSONBClassStructure jsonbClassStructure = context.getClassStructureByRootName(rootElement.getName());
        if (jsonbClassStructure==null) {
            throw new JSONBindingException("unable to bind name "+rootElement.getName()+" to a class in the scope");
        }
        Object result;
        try {
            result = jsonbClassStructure.getCurrentClass().newInstance();
        } catch (Exception e) {
            throw new JSONBindingException("unable to construct class "+jsonbClassStructure.getCurrentClass().getCanonicalName());
        }
        fillJSONObject(result, (JSONObject)rootElement.getValue(), jsonbClassStructure);
        return result;
    }
    
    
    @SuppressWarnings("unchecked")
    private void fillJSONObject(Object result,JSONObject obj,JSONBClassStructure st) throws JSONBindingException {
        
        for (JSONMember member : obj.getMembers()) {
            String currentName = member.getName();
            Object currentValue = member.getValue();
            final Object newObjectToFill;
            if (String.class.isAssignableFrom(currentValue.getClass()) ||
                    BigDecimal.class.isAssignableFrom(currentValue.getClass()) ||
                    Boolean.class.isAssignableFrom(currentValue.getClass()) 
                    ) {
                newObjectToFill = currentValue;
            }
            else if (JSONObject.class.isAssignableFrom(currentValue.getClass())){
                JSONBClassStructure expectedStructureForName = st.getChildStructureByChildName(currentName);
                if (expectedStructureForName==null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("definition of object associated with element name "+currentName+" not found");
                        continue;
                    }
                }
                try {
                    newObjectToFill = expectedStructureForName.getCurrentClass().newInstance();
                } catch (Exception e) {
                    throw new JSONBindingException("unable to instanciate class "+expectedStructureForName.getCurrentClass().getCanonicalName(),e);
                }               
                fillJSONObject(newObjectToFill, (JSONObject)currentValue, expectedStructureForName);
            }
            else if (JSONArray.class.isAssignableFrom(currentValue.getClass())) {
                JSONArray arrayToCopy = (JSONArray)currentValue;
                JSONBClassStructure listElementStructure = st.getChildStructureByChildName(currentName);
                Method listGetter = st.getGetterByElementName(currentName);
                final List<Object> listToInsertInto ;
                try {
                    listToInsertInto = (List<Object>) listGetter.invoke(result, (Object[])null);
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) {
                    throw new JSONBindingException("unable to retrieve the list associated to element name ="+currentName+" class="+result.getClass().getName());
                }
                for (Object valToCopy :arrayToCopy.getValues()) {
                    try {
                        Object newListElement = listElementStructure.getCurrentClass().newInstance();
                        fillJSONObject(newListElement, (JSONObject)valToCopy, listElementStructure);
                        listToInsertInto.add(newListElement);
                    } catch (InstantiationException | IllegalAccessException e) {
                        //
                    }
                }
                newObjectToFill = null;
            }
            else {
                throw new JSONBindingException("unsupported type found in json tree , type ="+currentValue.getClass().getCanonicalName());
            }
            Method setter = st.getSetterByElementName(currentName);
            if (setter!=null) {
                try {
                    setter.invoke(result, newObjectToFill);
                } catch (Exception e) {
                    throw new JSONBindingException("unable to fill property corresponding to element name = "+currentName,e);
                }
            }
            else if (newObjectToFill!=null) {
                throw new JSONBindingException("unable to fill property corresponding to element name = "+currentName);
            }
            
        }
    }

    private JSONBContext context;
    
    private final static Logger logger = Logger.getLogger(JSONBUnmarshaller.class);
    
}
